<?php if (!defined('CORE_DIR')) die('Wrong entry point!');

/* 
 * Copyright 2010 Anton Muraviev a.k.a kuromaisu
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */


// =============================================================================
// Section: URI
// =============================================================================
// 
// Configuration namespace:
// 	uri
// 
// Configuration options:
// 	routes	-	(array) route mapping rules (empty by default)

Aero::define('AeroUri', AS_CLASS);

/* -----------------------------------------------------------------------------
 * Class: AeroUri
 * -----------------------------------------------------------------------------
 *
 */
class AeroUri
{
	private static $rules = array();
	private static $rules_reverse = array();
	private $segments = array();

	/*
	 * Constructor: __construct
	 */
	public function __construct($segments)
	{
		if (empty($segments))
		{
			return;
		}
		
		if (!is_array($segments))
		{
			$segments = explode('/',$segments);
		}
		
		foreach ($segments as $segment)
		{
			$this->segments[] = urldecode($segment);
		}
	}
	
	/*
	 * Method: __toString
	 * 
	 * Returns:
	 * 	string
	 */
	public function __toString()
	{
		return $this->slice();
	}
	
	/*
	 * Method: slice
	 * 
	 * Parameters:
	 * 	int	-	first segment to return
	 * 	int	-	last segment to return
	 * 
	 * Returns:
	 * 	Forward slash-separated string of segments
	 */
	public function slice($first = null, $last = null)
	{
		return implode('/',$this->segments($first,$last));
	}
	
	/*
	 * Method: segments
	 * 
	 * Parameters:
	 * 	int	-	first segment to return
	 * 	int	-	last segment to return
	 * 
	 * Returns:
	 * 	Array of segments
	 */
	public function segments($first = null, $last = null)
	{
		if (!is_int($first))
		{
			return $this->segments;
		}
		
		if (!is_int($last))
		{
			return array_slice($this->segments, $first);
		}
		
		return array_slice($this->segments, $first, $last - $first + 1);
	}
	
	/*
	 * Method: keysAndValues
	 * 
	 * Reads part of the url as keys and values:
	 * 
	 * > // If uri is "some/segment/key0/value0/key1/value1/key2/value2"
	 * > $array = $uri->keysAndValues(2); // returns:
	 * > // array('key0' => 'value0','key1' => 'value1','key2' => 'value2');
	 * 
	 * Parameters:
	 * 	int	-	(optional) zero-based offset to start parsing
	 */
	public function keysAndValues($offset = 0)
	{
		if ($offset > count($this->segments))
		{
			return array();
		}
		
		$array = array();
		
		for ($i=$offset; $i < count($this->segments); $i+= 2)
		{ 
			$array[$this->segments[$i]] = (isset($this->segments[$i+1])) ? $this->segments[$i+1] : null;
		}
		
		return $array;
	}
	
	/*
	 * Method: segment
	 *
	 * Parameters:
	 * 	int		-	segment number
	 * 	string	-	(optional) default value
	 * 	string	-	(optional) validation constraints
	 * 	object	-	(optional) validation context
	 * 
	 * Returns:
	 * 	string
	 */
	public function segment($number, $default = false, $constraints = '', $context = null)
	{
		$value = isset($this->segments[$number]) ? $this->segments[$number] : $default;
		
		if (strlen($constraints) > 0)
		{
			$validator = Aero::load('libs/validator');

			if (true !== ($error = $validator->run($value, $constraints, $context)))
			{
				array_shift($error['arguments']);
				
				throw new RequestException('Segment #'.$number.' failed validation rule: '.$error['rule'].(count($error['arguments']) > 0 ? '['.implode(', ',$error['arguments']).']' : '').'.');
			}
		}
		
		return $value;
	}
	
	/*
	 * Method: route
	 * 
	 * Takes the uri path and attemps to find the respective controller file 
	 * within the $base directory. When found the controller is loaded using
	 * Aero::load(). If the controller is an object one of its method is
	 * executed.
	 * 
	 * If the controller object has _remap() method, it is executed first. It
	 * should accept original method name as 1st parameter and return the name
	 * of the method that will be called.
	 * 
	 * Returns:
	 * 	misc	-	Whatever controller method returned or void.
	 * 
	 * Throws:
	 * 	RequestException	-	whenever something went wrong.
	 */
	public function route($base, $default_controller = 'index')
	{
		$base = aero_trim_path($base);
		$full = Aero::basePath().$base;
		
		// Base path should exist
		if (!file_exists($full))
		{
			throw new RequestException('Directory does not exist: '.$base);
		}
		
		$path = '';
		$segments = $this->segments();
		
		// Search for a controller file
		do {
			// If no segments, try loading default controller
			if ($path === '' && count($segments) === 0)
			{
				$path = '/'.aero_trim_path($default_controller);
			}
			else
			{
				$path.= '/'.array_shift($segments);
			}
			
			if (is_file($full.$path.EXT))
			{
				$controller = Aero::load($base.$path);
				
				if (is_object($controller) && method_exists($controller, 'respondTo'))
				{
					$controller->respondTo(Aero::load('libs/uri',$segments), $this);
				}
				
				return;
			}
			else if (!is_dir($full.$path))
			{
				throw new RequestException('No such file or directory: '.Aero::basePath().$base.$path);
			}
		} while (count($segments) > 0);
		
		throw new RequestException('Could not route '.$path);
	}
	
	/*
	 * Method: rewrite
	 * 
	 * Parameters:
	 * 	string	-	uri to rewrite
	 * 	array	-	rewrite rules
	 * 
	 * Returns:
	 * 	string
	 */
	public static function rewrite($uri, $reverse = false)
	{
		$uri = trim($uri,'/');
		$rules = Aero::option('uri.routes',array());
		
		if (count($rules) == 0 || strlen($uri) == 0)
		{
			return $uri;
		}
		
		if (empty(self::$rules) || empty(self::$rules_reverse))
		{
			uksort($rules, array('AeroUri','_callbackSortRules'));
			
			$tokens = array(
				':num:' => '(\d+)',
				':word:' => '([\w\-_]+)',
				':any:' => '([a-zA-Z0-9\-_]+)'
			);
			
			foreach ($rules as $before => $after)
			{
				$before = trim($before,'/');
				$after = trim($after,'/');
				
				preg_match_all('~(?:'.implode('|',array_keys($tokens)).')~', 
					$before, 
					$backrefs, 
					PREG_OFFSET_CAPTURE);
				$backrefs = $backrefs[0];
				
				preg_match_all('~:(\d):~', $after, $indexes);
				$indexes = $indexes[1];
				
				if (count($backrefs) === 0)
				{
					self::$rules['~^'.$before.'~'] = $after;
					self::$rules_reverse['~^'.$after.'~'] = $before;
					continue;
				}
				
				// Forward rules
				$from = '~^'.str_replace(array_keys($tokens),$tokens,$before).'~';
				
				self::$rules[$from] = preg_replace('~:(\d+):~','\\\\\1',$after);
				
				// Reverse rules
				$diff = 0;
				
				foreach ($backrefs as $index => $backref)
				{
					list($token, $offset) = $backref;
					$offset+= $diff;
					
					$after = str_replace(':'.($index + 1).':', $tokens[$token], $after);
					$before = substr_replace($before, '\\'.$indexes[$index], $offset, strlen($token));
					$diff+= strlen('\\'.$index) - strlen($token);
				}
				
				self::$rules_reverse['~^'.$after.'~'] = $before;
			}
		}
		
		if (!$reverse)
		{
			return preg_replace(array_keys(self::$rules),self::$rules,$uri);
		}
		else
		{
			return preg_replace(array_keys(self::$rules_reverse),self::$rules_reverse,$uri);
		}
	}
	
	public static function _callbackSortRules($string_a, $string_b)
	{
		return strlen($string_b) - strlen($string_a);
	}
	
	/*
	 * Method: make
	 * 
	 * Converts all supplied arguments into uri segments and combines into correct URL.
	 * 
	 * Parameters:
	 * 	string || array
	 * 
	 * Returns:
	 * 	string
	 */
	public static function make()
	{
		$uri = array();
		$args = func_get_args();
		
		foreach ($args as $arg)
		{
			if (is_array($arg))
			{
				if (count($arg) == 0)
				{
					continue;
				}
				
				if (array_values($arg) === $arg)
				{
					$uri[] = self::_arrayToUri($arg);
				}
				else
				{
					foreach ($arg as $key => $value)
					{
						if (empty($key) || empty($value))
						{
							continue;
						}
						
						$uri[] = urlencode($key);
						
						if (is_array($value))
						{
							$uri[] = self::_arrayToUri($value);
						}
						else
						{
							$uri[] = urlencode($value);
						}
					}
				}
			}
			else
			{
				$uri[] = trim($arg,'/');
			}
		}
		
		return '/'.self::rewrite(implode('/',$uri),true);
	}
	
	/*
	 * Method: _arrayToUri
	 */
	protected static function _arrayToUri($array)
	{
		$segments = array();
		
		foreach ($array as $value)
		{
			$segments[] = urlencode($value);
		}
		
		return implode(',',$segments);
	}
}